<template>
    <div class="puzzle">
        <img crossorigin="" hidden :src="state.img" alt="" id="puzzle-img" />
        <canvas
            id="puzzle-background"
            :width="width"
            :height="height"
            style="background:transparent;"
        ></canvas>
        <canvas
            :style="slideOffsetComputed"
            width="50"
            height="50"
            id="puzzle-slide"
        ></canvas>
        <canvas
            :style="goalOffsetComputed"
            width="50"
            height="50"
            id="puzzle-goal"
        ></canvas>

        <!-- 

    @mousemove="onMouseMove"
            @mouseleave="onMouseLeave"
            @mouseup="onMouseUp"
 -->
        <div class="puzzle-box">
            <div class="puzzle-handle">
                <div
                    :style="puzzleButtonComputed"
                    class="puzzle-button"
                    @mousedown="onMouseDown"
                >
                    <svg
                        xmlns="http://www.w3.org/2000/svg"
                        xmlns:xlink="http://www.w3.org/1999/xlink"
                        viewBox="0 0 24 24"
                    >
                        <path
                            d="M10 17l5-5l-5-5v10z"
                            fill="currentColor"
                        ></path>
                    </svg>
                </div>
            </div>
        </div>
        <div :class="puzzleResultClassComputed">
            {{ puzzleResultTextComputed }}
        </div>
        <div class="puzzle-refresh" @click="reset">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                xmlns:xlink="http://www.w3.org/1999/xlink"
                viewBox="0 0 24 24"
            >
                <path
                    d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
                    fill="currentColor"
                ></path>
            </svg>
        </div>
    </div>
</template>

<script setup>
import {
    onMounted,
    computed,
    reactive,
    toRef,
    unref,
    onBeforeUnmount,
} from "vue";

const emits = defineEmits(["complete"]);

const props = defineProps({
    width: {
        type: Number,
        default: 300,
    },
    height: {
        type: Number,
        default: 150,
    },

    offsetDistance: {
        type: Number,
        default: 10,
    },
    successText: {
        type: String,
        default: "校验成功",
    },
    failText: {
        type: String,
        default: "校验失败",
    },
    img: {
        type: String,
        default: "https://i.loli.net/2021/08/30/lj1ciV3An8JDo6u.jpg",
    },

    errorRetry: {
        type: Boolean,
        default: true,
    },
});

const width = toRef(props, "width");
const height = toRef(props, "height");

const state = reactive({
    img: unref(props.img),
    backgroundContext: null,
    slideContext: null,
    goalContext: null,

    offsetX: 0,
    offsetY: 0,

    isMousedown: false,
    mouseClientX: 0,

    slideOffsetX: 10,
    distance: 0,

    result: "default",
});

onMounted(() => {
    initMoveWatcher();
    initCanvasContext();
    drawBackgroundImage();
});

onBeforeUnmount(() => {
    removeMoveWatcher();
});

/**
 * @computed 计算属性
 */

const goalOffsetComputed = computed(() => ({
    left: `${state.offsetX}px`,
    top: `${state.offsetY}px`,
}));

const slideOffsetComputed = computed(() => ({
    left: `${state.slideOffsetX + state.distance + 10}px`,
    top: `${state.offsetY}px`,
}));

const puzzleButtonComputed = computed(() => ({
    left: `${state.distance}px`,
}));

const puzzleResultClassComputed = computed(() => [
    "puzzle-result",
    (state.result == "success" || state.result == "fail") &&
        `puzzle-result-${state.result}`,
]);

const puzzleResultTextComputed = computed(() => {
    const TEXT_ENMU = { success: props.successText, fail: props.failText };
    return TEXT_ENMU[state.result];
});

/**
 * @methods 方法
 */

function initMoveWatcher() {
    const body = document.body;
    body.addEventListener("mouseleave", onMouseLeave);
    body.addEventListener("mousemove", onMouseMove);
    body.addEventListener("mouseup", onMouseUp);
}

function removeMoveWatcher() {
    const body = document.body;
    body.removeEventListener("mouseleave", onMouseLeave);
    body.removeEventListener("mousemove", onMouseMove);
    body.removeEventListener("mouseup", onMouseUp);
}

function initCanvasContext() {
    state.backgroundContext = getCanvasContext("#puzzle-background");
    state.slideContext = getCanvasContext("#puzzle-slide");
    state.goalContext = getCanvasContext("#puzzle-goal");
}

function getCanvasContext(selector) {
    const canvas = document.querySelector(selector);
    return canvas.getContext("2d");
}

async function drawBackgroundImage() {
    const img = await getImage();
    if (img) {
        const scale = props.width / props.height;
        state.backgroundContext.drawImage(
            img,
            0,
            0,
            img.width,
            img.width / scale,
            0,
            0,
            props.width,
            props.height
        );
        drawSlideImage();
    }
}

function getImage() {
    return new Promise(resolve => {
        const img = document.getElementById("puzzle-img");
        img.onload = () => {
            resolve(img || false);
        };
    });
}

function getBackgroundRandomArea() {
    const x = ~~(Math.random() * 80 + 140);
    const y = ~~((props.height - 50) / 2);
    const imgData = state.backgroundContext.getImageData(x, y, 50, 50);
    state.offsetX = 10 + x;
    state.offsetY = 10 + y;
    return imgData;
}

function drawSlideImage() {
    const imgData = getBackgroundRandomArea();
    state.slideContext.putImageData(imgData, 0, 0);
}

function onMouseDown({ clientX }) {
    state.isMousedown = true;
    state.mouseClientX = clientX;
}

function onMouseUp() {
    resultVerify();
    state.isMousedown = false;
    state.mouseClientX = null;
}

function onMouseMove({ clientX }) {
    if (state.isMousedown) {
        if (clientX - state.mouseClientX > 0) {
            state.distance = clientX - state.mouseClientX;
        }
    }
}

function onMouseLeave() {
    onMouseUp();
}

/**
 * @description
 * GAP 代表SLIDE 距离 GOAL 的偏差,其中 offsetX是生成GOAL的style left距离,
 * 注意：是距离最外层父元素的距离而不是BACKGROUND的距离,减去10是因为BACKGROUND
 * 距离最外层父元素有10间距偏差,然后this.slideOffsetX + this.distance是SLIDE
 * 真正距离BACKGROUND的距离,因为拖动按钮是从BACKGROUND对齐的.
 * 再次设置isMousedown = false,是因为校验完成之后不允许再次拖动,应该手动调用reset方法恢复出厂设置重新进行验证;
 */

function resultVerify() {
    if (state.isMousedown) {
        const GAP = Math.abs(
            state.offsetX - 10 - (state.slideOffsetX + state.distance)
        );
        state.result = GAP <= props.offsetDistance ? "success" : "fail";
        state.isMousedown = false;
        if (props.errorRetry && state.result == "fail") {
            setTimeout(reset, 1000);
        }
        emits("complete", state.result);
    }
}

function reset() {
    resetState();
    drawSlideImage();
    drawBackgroundImage();
}
function resetState() {
    state.isMousedown = false;
    state.mouseClientX = 0;
    state.distance = 0;
    state.result = "default";
}
</script>

<style scoped>
.puzzle {
    padding: 10px;
    display: inline-block;
    background-color: #fff;
    border: 1px solid #f1f1f1;
    position: relative;
}

#puzzle-background {
}
#puzzle-slide {
    position: absolute;
    z-index: 5;
    background-color: transparent;
    box-shadow: 0 0 4px 0.8px rgba(255, 255, 255, 0.3);
}
#puzzle-goal {
    position: absolute;
    background-color: #fff;
    bottom: 50px;
    box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.3) inset;
}

.puzzle-box {
    height: 40px;
    margin-top: 7px;
    border: 1px solid #f1f1f1;
    position: relative;
    user-select: none;
}

.puzzle-handle {
    position: absolute;
}

.puzzle-button {
    position: absolute;
    height: 40px;
    width: 40px;
    background-color: #fff;
    box-sizing: border-box;
    box-shadow: 0 0 2px 1px lightgray;
    text-align: center;
    line-height: 40px;
    font-size: 14px;
    cursor: w-resize;
    color: #555;
    font-weight: bold;
}

.puzzle-button:hover {
    color: #000;
    font-weight: bold;
}

.puzzle-result {
    width: calc(100% - 20px);
    position: absolute;
    height: 0px;
    z-index: 6;
    bottom: 60px;
    transition: height 0.2s ease-out;
    text-align: center;
    color: #fff;
    font-size: 12px;
    font-family: 微软雅黑;
}

.puzzle-result-success {
    background-color: rgba(52, 189, 104, 1);
    height: 20px;
    line-height: 20px;
}
.puzzle-result-fail {
    background-color: #f13a3a;
    height: 20px;
    line-height: 20px;
}

.puzzle-refresh {
    position: absolute;
    right: 20px;
    top: 20px;
    width: 20px;
    height: 20px;
    cursor: pointer;
    background: #fff;
    border-radius: 50%;
}
</style>
